/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2020 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

"use strict";

var ui = angular.module('axelor.ui');

var equals = angular.equals,
  forEach = angular.forEach,
  isArray = angular.isArray,
  isObject = angular.isObject,
  isDate = angular.isDate;

function dummyEquals(a, b) {
  if (a === b) return true;
  if (a === null || b === null) return false;
  if (a !== a && b !== b) return true; // NaN === NaN
  var keys = _.keys(a).filter(function (k) { return k.indexOf('$') === 0; });
  if (keys.length === 0) {
    return true;
  }
  for (var i = 0; i < keys.length; i++) {
    var k = keys[i];
    if (!equals(a[k], b[k])) {
      return false;
    }
  }
  return true;
}

function updateValues(source, target, itemScope, formScope) {

  if (equals(source, target) && dummyEquals(source, target) && (!source || !source.$force)) {
    return;
  }

  // handle json records
  if (source && formScope && formScope._model === 'com.axelor.meta.db.MetaJsonRecord') {
    if (source.attrs || source.id) {
      source = source.id > 0
        ? _.pick(source, 'jsonModel', 'name', 'attrs', 'id', 'version')
        : _.pick(source, 'jsonModel', 'name', 'attrs');
    }

    var values = source.attrs ? _.extend({}, JSON.parse(source.attrs)) : source;
    var fix = function (rec) {
      if (!rec) return rec;
      if (_.isArray(rec)) return _.map(rec, fix);
      if (rec.id > 0 && (rec.version || rec.attrs)) {
        rec = _.pick(rec, 'id', 'name', 'selected');
        if (!rec.selected) delete rec.selected;
      }
      return rec;
    };

    _.each(values, function (v, k) {
      values[k] = fix(v);
    });

    // if called from form fields
    if (itemScope && itemScope.updateJsonValues) {
      return itemScope.updateJsonValues(values);
    }

    // onNew or onSave from main form
    var current = target && target.attrs ? JSON.parse(target.attrs) : {};
    if (source.attrs || !source.jsonModel) {
      source.attrs = JSON.stringify(_.extend({}, current, values));
    }

  } else if (itemScope && itemScope.updateJsonValues) {
    return itemScope.updateJsonValues(source);
  }

  function compact(value) {
    if (!value) return value;
    if (value.version === undefined) return value;
    if (!value.id) return value;
    var res = _.extend(value);
    res.$version = res.version;
    res.version = undefined;
    return res;
  }

  var changed = false;

  forEach(source, function(value, key) {
    var dest;
    var newValue = value;
    var oldValue = target[key];
    if (oldValue === newValue) {
      return;
    }
    if (isArray(value)) {
      dest = target[key] || [];
      newValue = _.map(value, function(item) {
        var found = _.find(dest, function(v){
          return item.id && v.id === item.id;
        });
        if (_.has(item, "version") && item.id) item.$fetched = true;
        if (found) {
          var found_ = _.extend({}, found);
          var changed_ = updateValues(item, found_);
          changed = changed || changed_;
          return changed_ ? found_ : found;
        }
        return item;
      });
    } else if (isObject(value) && !isDate(value)) {
      dest = target[key] || {};
      if (dest.id === value.id) {
        if (_.isNumber(dest.version)) {
          dest = _.extend({}, dest);
          changed = updateValues(value, dest, itemScope, formScope) || changed;
        } else {
          dest.$updatedValues = value;
          if (formScope) {
            formScope.$broadcast('on:check-nested-values', value);
          }
        }
      } else {
        dest = compact(value);
      }
      newValue = dest;
    }

    if (!equals(oldValue, newValue)) {
      changed = true;
      target[key] = newValue;
    }
  });

  if (target && changed) {
    target.$dirty = true;
  }

  return changed;
}

function handleError(scope, item, message) {

  if (!item) {
    return;
  }

  var ctrl = item.data('$ngModelController');
  if (!ctrl) {
    return;
  }

  if (ctrl.$doReset) {
    ctrl.$doReset();
  }

  if (!message) {
    ctrl.$doReset = null;
    return;
  }

  var e = $('<span class="error"></span>').text(message);
  var p = item.parent('.form-item');

  if (item.children(':first').is(':input,.input-append,.picker-input')) {
    p.append(e);
  } else {
    p.prepend(e);
  }

  var clear = scope.$on('on:edit', function(){
    ctrl.$doReset();
  });

  function cleanUp(items) {
    var idx = items.indexOf(ctrl.$doReset);
    if (idx > -1) {
      items.splice(idx, 1);
    }
  }

  ctrl.$doReset = function(value) {

    cleanUp(ctrl.$viewChangeListeners);
    cleanUp(ctrl.$formatters);

    ctrl.$setValidity('invalid', true);
    ctrl.$doReset = null;

    e.remove();
    clear();

    return value;
  };

  if (!item.hasClass('readonly')) {
    ctrl.$setValidity('invalid', false);
  }
  ctrl.$viewChangeListeners.push(ctrl.$doReset);
  ctrl.$formatters.push(ctrl.$doReset);
}

function ActionHandler($scope, ViewService, options) {

  if (!options || !options.action)
    throw 'No action provided.';

  this.canSave = options.canSave;
  this.name = options.name;
  this.prompt = options.prompt;
  this.action = options.action;
  this.element = options.element || $();

  this.scope = $scope;
  this.ws = ViewService;
  this.viewType = $scope.viewType;
}

ActionHandler.prototype = {

  constructor: ActionHandler,

  onLoad : function() {
    return this.handle();
  },

  onNew: function() {
    return this.handle();
  },

  onSave: function() {
    var self = this;
    return this._fireBeforeSave().then(function() {
      return self.handle();
    });
  },

  onTabSelect: function(unblocked) {
    return this.onSelect.apply(this, arguments);
  },

  onSelect: function(unblocked) {
    var self = this;
    var blockUI = this._blockUI;
    if (unblocked) {
      this._blockUI = angular.noop;
    }
    function reset() {
      self._blockUI = blockUI;
    }
    var promise = this.handle();
    promise.then(reset, reset);
    return promise;
  },

  onClick: function(event) {
    var self = this;
    var prompt = this._getPrompt();
    if (prompt) {
      var deferred = this.ws.defer(),
        promise = deferred.promise;
      axelor.dialogs.confirm(prompt, function(confirmed){
        if (confirmed) {
          self._fireBeforeSave().then(function() {
            self.handle().then(deferred.resolve, deferred.reject);
          });
        } else {
          self.scope.$timeout(deferred.reject);
        }
      }, {
        yesNo: false
      });
      return promise;
    }
    return this._fireBeforeSave().then(function() {
      return self.handle();
    });
  },

  onChange: function(event) {
    return this.handle({ wait: 100 });
  },

  _getPrompt: function () {
    var prompt = this.prompt;
    var itemScope = this.element.scope();
    if (_.isFunction(itemScope.attr) && !this.element.is('[ui-slick-grid]')) {
      prompt = itemScope.attr('prompt') || prompt;
    }
    return _.isString(prompt) ? prompt : null;
  },

  _getContext: function() {
    var scope = this.scope,
      context = scope.getContext ? scope.getContext() : scope.record,
      viewParams = scope._viewParams || {};

    context = _.extend({}, viewParams.context, context);
    if (context._model === undefined) {
      context._model = scope._model;
    }

    // include button name as _signal (used by workflow engine)
    if (this.element.is("button,a.button-item,li.action-item")) {
      context._signal = this.element.attr('name') || this.element.attr('x-name');
    }

    return context;
  },

  _getRootFormElement: function () {
    var formElement = $(this.element).parents('form[ui-form]:last');
    if (formElement.length === 0) {
      formElement = this._getFormElement();
    }
    return formElement;
  },

  _getFormElement: function () {

    var elem = $(this.element);
    var formElement = elem;

    if (formElement.is('form')) {
      return formElement;
    }

    formElement = elem.data('$editorForm') || elem.parents('form:first');
    if (!formElement || !formElement.get(0)) { // toolbar button
      formElement = this.element.parents('.form-view:first').find('form:first');
    }
    if (formElement.length === 0) {
      formElement = this.element;
    }
    return formElement;
  },

  handle: function(options) {
    var that = this;
    var action = this.action.trim();
    var deferred = this.ws.defer();

    var all = this.scope.$actionPromises || [];
    var pending = all.slice();
    var opts = _.extend({}, options);

    all.push(deferred.promise);

    this.scope.waitForActions(function () {
      var promise = that._handleAction(action);
      function done() {
        setTimeout(function () {
          var i = all.indexOf(deferred.promise);
          if (i > -1) {
            all.splice(i, 1);
          }
        }, 10);
      }
      promise.then(done, done);
      promise.then(deferred.resolve, deferred.reject);
    }, opts.wait || 10, pending);

    return deferred.promise;
  },

  _blockUI: function() {
    // block the entire ui (auto unblocks when actions are complete)
    _.delay(axelor.blockUI, 10);
  },

  _fireBeforeSave: function() {
    var scope = this._getRootFormElement().scope();
    var event = scope.$broadcast('on:before-save', scope.record);
    var deferred = this.ws.defer();

    if (event.defaultPrevented) {
      if (event.error) {
        axelor.dialogs.error(event.error);
      }
      setTimeout(function() {
        deferred.reject(event.error);
      });
    } else {
      scope.$timeout(function() {
        scope.ajaxStop(function() {
          deferred.resolve();
        }, 100);
      }, 50);
    }
    return deferred.promise;
  },

  _checkVersion: function() {
    var self = this;
    var scope = this.scope;
    var deferred = this.ws.defer();

    if (scope.checkVersion) {
      scope.checkVersion(function (verified) {
        if (verified) {
          return deferred.resolve();
        }
        axelor.dialogs.error(
            _t("The record has been updated or delete by another action."));
        deferred.reject();
      });
    } else {
      deferred.resolve();
    }

    return deferred.promise;
  },

  _handleNew: function() {
    var self = this;
    var scope = this.scope;
    var deferred = this.ws.defer();

    if (scope.onNew) {
      return scope.onNew();
    }
    if (scope.editRecord) {
      scope.editRecord(null);
      deferred.resolve();
    } else {
      deferred.reject();
    }

    return deferred.promise;
  },

  _handleSave: function(validateOnly) {
    if (validateOnly) {
      return this.__handleSave(validateOnly);
    }
    var self = this;
    var deferred = this.ws.defer();

    this._checkVersion().then(function () {
      self.__handleSave().then(deferred.resolve, deferred.reject);
    }, deferred.reject);

    return deferred.promise;
  },

  __handleSave: function(validateOnly) {
    var self = this;
    var scope = this.scope;
    var id = (scope.record||{}).id;
    var o2mPopup = scope._isPopup && (scope.$parent.field||{}).serverType === "one-to-many";
    if (o2mPopup && !validateOnly && this.name == 'onLoad' && (!id || id < 0)) {
      var deferred = this.ws.defer();
      var msg = _t("The {0}={1} event can't call 'save' action on unsaved o2m item.", this.name, this.action);
      deferred.reject(msg);
      console.error(msg);
      return deferred.promise;
    }
    return this._fireBeforeSave().then(function() {
      return self.__doHandleSave(validateOnly);
    });
  },

  __doHandleSave: function(validateOnly) {

    this._blockUI();

    // save should be done on root form scope only
    var rootForm = this._getRootFormElement();
    var scope = rootForm.is('[ui-view-grid]') ? this.scope : rootForm.scope();
    var deferred = this.ws.defer();

    if (scope.isValid && !scope.isValid()) {
      if (scope.showErrorNotice) {
        scope.showErrorNotice();
      } else {
        axelor.notify.error(_t('Please correct the invalid form values.'), {
          title: _t('Validation error')
        });
      }
      deferred.reject();
      return deferred.promise;
    }
    if (validateOnly || (scope.isDirty && !scope.isDirty())) {
      deferred.resolve();
      return deferred.promise;
    }

    function doEdit(rec) {
      var params = scope._viewParams || {};
      scope.editRecord(rec);
      if (params.$viewScope) {
        params.$viewScope.updateRoute();
      }
      deferred.resolve();
    }

    function doSave(values) {
      var ds = scope._dataSource;
      ds.save(values).success(function(rec, page) {
        if (scope.doRead) {
          return scope.doRead(rec.id).success(doEdit);
        }
        return ds.read(rec.id).success(doEdit);
      });
    }

    var values = _.extend({ _original: scope.$$original }, scope.record);
    if (scope.onSave) {
      scope.onSave({
        values: values,
        callOnSave: false,
        wait: false
      }).then(deferred.resolve, deferred.reject);
    } else {
      doSave(values);
    }

    this._invalidateContext = true;
    return deferred.promise;
  },

  _closeView: function (scope) {
    if (scope.onOK) {
      return scope.onOK();
    }
    var tab = scope._viewParams || scope.selectedTab;
    if (scope.closeTab) {
      scope.closeTab(tab);
    } else if (scope.$parent) {
      this._closeView(scope.$parent);
    }
  },

  _isSameViewType: function () {
    return this.viewType === this.scope.viewType;
  },

  _handleAction: function(action) {

    this._blockUI();

    var self = this,
      scope = this.scope,
      context = this._getContext(),
      deferred = this.ws.defer();

    if (!this._isSameViewType()) {
      deferred.reject();
      return deferred.promise;
    }

    function resolveLater() {
      deferred.resolve();
      return deferred.promise;
    }

    function chain(items) {
      var first = _.first(items);
      if (first === undefined) {
        return resolveLater();
      }
      return self._handleSingle(first).then(function(pending) {
        if (_.isString(pending) && pending.trim().length) {
          return self._handleAction(pending);
        }

        var _deferred = self.ws.defer();
        scope.$timeout(function () {
          scope.ajaxStop(function() {
            _deferred.resolve();
          });
        });

        return _deferred.promise.then(function () {
          return chain(_.rest(items));
        });
      });
    }

    if (!action) {
      return resolveLater();
    }

    action = action.replace(/(^\s*,?\s*)|(\s*,?\s*$)/, '');

    var pattern = /,\s*(sync)\s*(,|$)/;
    if (pattern.test(action)) {
      var which = pattern.exec(action)[1];
      axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the first action.', which));
      deferred.reject();
      return deferred.promise;
    }

    pattern = /(^sync\s*,\s*)|(^sync$)/;
    if (pattern.test(action)) {
      action = action.replace(pattern, '');
      return this._fireBeforeSave().then(function() {
        return self._handleAction(action);
      });
    }

    pattern = /(^|,)\s*(new)\s*,/;
    if (pattern.test(action)) {
      var which = pattern.exec(action)[2];
      axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the last action.', which));
      deferred.reject();
      return deferred.promise;
    }

    pattern = /(^|,)\s*(close)\s*,/;
    if (pattern.test(action)) {
      axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the last action.', pattern.exec(action)[2]));
      deferred.reject();
      return deferred.promise;
    }

    if (action === 'close') {
      this._closeView(scope);
      deferred.resolve();
      return deferred.promise;
    }

    if (action === 'new') {
      return this._handleNew();
    }

    if (action === 'validate') {
      return this._handleSave(true);
    }

    if (action === 'save') {
      return this._handleSave();
    }

    if (this._invalidateContext) {
      context = this._getContext();
      this._invalidateContext = false;
    }

    var model = context._model || scope._model;
    var data =  scope.getActionData ? scope.getActionData(context) : null;
    if (data && context._signal) {
      data._signal = context._signal;
    }

    var promise = this.ws.action(action, model, context, data).then(function(response){
      var resp = response.data,
        data = resp.data || [];
      if (resp.errors) {
        data.splice(0, 0, {
          errors: resp.errors
        });
      }
      return chain(data);
    });

    promise.then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  _handleSingle: function(data) {

    var deferred = this.ws.defer();

    if (!data || data.length === 0) {
      deferred.resolve();
      return deferred.promise;
    }

    if (!this._isSameViewType()) {
      deferred.reject();
      return deferred.promise;
    }

    var self = this,
      scope = this.scope,
      formElement = this._getFormElement(),
      formScope = formElement.data('$scope') || scope,
      rootForm = this._getRootFormElement(),
      rootScope = rootForm.is('[ui-view-grid]') ? scope : rootForm.scope();

    function doReload(pending) {
      self._invalidateContext = true;
      var promise = _.isFunction(rootScope.reload) ? rootScope.reload() : scope.reload();
      if (promise) {
        promise.then(function(){
          deferred.resolve(pending);
        }, deferred.reject);
      } else {
        deferred.resolve(pending);
      }
      return deferred.promise;
    }

    if (data.exportFile) {
      (function () {
        var link = "ws/files/data-export/" + data.exportFile;
        var frame = $('<iframe>').appendTo('body').hide();
        frame.attr("src", link);
        setTimeout(function(){
          frame.attr("src", "");
          frame.remove();
          frame = null;
        }, 5000);
      })();
    }

    if (data.signal === 'refresh-app') {
      if(data.flash || data.info) {
        axelor.dialogs.box(data.flash || data.info, {
          onClose: function () {
            window.location.reload();
          }
        });
      } else {
        window.location.reload();
      }
      return deferred.promise;
    }

    if(data.flash || data.info) {
      axelor.dialogs.box(data.flash || data.info, {
        onClose: function () {
          if (data.pending) {
            scope.$applyAsync(function(){
              if (data.reload) {
                return doReload(data.pending);
              }
              deferred.resolve(data.pending);
            });
          }
        }
      });
      if (data.pending) {
        return deferred.promise;
      }
    }

    if(data.notify) {
      axelor.notify.info(data.notify);
    }

    if(data.error) {
      axelor.dialogs.error(data.error, function(){
        scope.$applyAsync(function(){
          if (data.action) {
            self._handleAction(data.action);
          }
          deferred.reject();
        });
      });
      return deferred.promise;
    }

    if (data.alert) {
      axelor.dialogs.confirm(data.alert, function(confirmed){
        scope.$applyAsync(function(){
          if (confirmed) {
            return deferred.resolve(data.pending);
          }
          if (data.action) {
            self._handleAction(data.action);
          }
          deferred.reject();
        });
      }, {
        title: _t('Warning'),
        yesNo: false
      });

      return deferred.promise;
    }

    if (!_.isEmpty(data.errors)) {
      var hasError = false;
      _.each(data.errors, function(v, k){
        var item = (findItems(k) || $()).first();
        handleError(scope, item, v);
        if(v && v.length > 0) {
          hasError = true;
        }
      });
      if(hasError) {
        deferred.reject();
        return deferred.promise;
      }
    }

    if (data.values) {
      updateValues(data.values, scope.record, scope, formScope);
      if (scope.onChangeNotify) {
        scope.onChangeNotify(scope, data.values);
      }
      this._invalidateContext = true;
      axelor.$adjustSize();
    }

    if (data.reload) {
      return (function () {
        var promise = doReload(data.pending);
        if (data.view) {
          promise.then(function () {
            doOpenView(data.view);
          });
        }
        return promise;
      })();
    }

    if (data.validate || data.save) {
      scope.$timeout(function () {
        self._handleSave(!!data.validate).then(function(){
          scope.ajaxStop(function () {
            deferred.resolve(data.pending);
          }, 100);
        }, deferred.reject);
      });
      return deferred.promise;
    }

    if (data['new']) {
      scope.$timeout(function () {
        self._handleNew().then(function(){
          scope.ajaxStop(function () {
            deferred.resolve(data.pending);
          }, 100);
        }, deferred.reject);
      });
      return deferred.promise;
    }

    if (data.signal) {
      formScope.$broadcast(data.signal, data['signal-data']);
    }

    function findItems(name) {

      var items;
      var toolbar;
      var containers;

      if (formElement.is('[ui-slick-editors]')) {
        containers = formElement.parent().add(formElement);
      } else if (formElement.parent().is('[ui-slick-editors],.slick-cell')) {
        containers = formElement.parent().parent().add(formElement);
      } else if (formElement.parent().is('[ui-panel-editor]')) {
        containers = formElement.parent().add(formElement).is('.m2o-editor-form,.o2m-editor-form') ? formElement : formElement.parents('[ui-form]:first').add(formElement);
      } else {
        containers = formElement;
        toolbar = formElement.parents('.form-view:first,.search-view:first')
          .find('.record-toolbar:first,.search-view-toolbar:first');
      }

      var formPath = formScope.formPath;
      if (formScope._model === 'com.axelor.meta.db.MetaJsonRecord') {
        formPath = formPath || 'attrs';
      }

      items = containers.find('[x-path="' + (formPath ?  formPath + '.' + name : name) + '"]');
      if (items.length === 0 && formPath != 'attrs') {
        items = containers.find('[x-path="attrs.' + name + '"]');
      }
      if (toolbar) {
        return toolbar.find('[name="' + name + '"],[x-name="' + name + '"]').add(items);
      }
      return items;
    }

    function setAttrs(item, itemAttrs, itemIndex) {

      var label = item.data('label'),
        itemScope = item.data('$scope'),
        hasValues = false,
        column;

      if (item.is('[ui-menu-item]')) {
        itemScope = item.isolateScope();
      }

      // handle o2m/m2m columns
      if (item.is('.slick-dummy-column')) {
        column = item.data('column');
        itemScope = item.parents('[x-path]:first,.portlet-grid').data('$scope');
        forEach(itemAttrs, function(value, attr){
          if (attr == 'hidden')
            itemScope.showColumn(column.id, !value);
          if (attr == 'title')
            setTimeout(function(){
              itemScope.setColumnTitle(column.id, value);
            });
        });
        return;
      }

      //handle o2m/m2m title
      if(item.is('.one2many-item') || item.is('.many2many-item')){
        forEach(itemAttrs, function(value, attr){
          if (attr == 'title') {
            itemScope.title = value;
          }
        });
      }

      // handle notebook
      if (item.is('.tab-pane')) {
        forEach(itemAttrs, function(value, attr){
          if (attr == 'hidden') {
            itemScope.attr('hidden', value);
          }
          if (attr == 'title') {
            itemScope.title = value;
          }
        });
        return;
      }

      function isDotted() {
        var name = item.attr('x-field') || '';
        var dotted = name.indexOf('.') > -1;
        if (dotted) {
          itemAttrs.$hasDotted = true;
        }
        return dotted;
      }

      forEach(itemAttrs, function(value, attr){

        if ((attr === "value" || attr.indexOf('value:') === 0)) {
          hasValues = true;
          if (itemScope && itemScope.$setForceWatch) {
            itemScope.$setForceWatch(true);
          }
          if (isDotted()) return;
          if (itemAttrs.$hasDotted) {
            itemAttrs.$hasDotted = false;
          } else if (itemIndex > 0) {
            return;
          }
        }

        switch(attr) {
        case 'hidden':
          if (itemScope.field && itemScope.field.hideIf === "true") return;
        case 'required':
        case 'readonly':
        case 'collapse':
        case 'precision':
        case 'scale':
        case 'prompt':
        case 'css':
        case 'icon':
        case 'selection-in':
          itemScope.attr(attr, value);
          break;
        case 'title':
          (function () {
            var span = $(label).add(item).children('span[ui-help-popover]:first');
            if (span.length === 0) {
              span = label;
            }
            if (span && span.length > 0) {
              span.html(value);
            } else if (item.is('label')) {
              item.html(value);
            }
          })();
          itemScope.attr('title', value);
          break;
        case 'domain':
          if (itemScope.setDomain)
            itemScope.setDomain(value);
          break;
        case 'refresh':
          itemScope.waitForActions(function () {
            itemScope.$broadcast('on:attrs-change:refresh');
          }, 100);
          break;
        case 'url':
        case 'url:set':
          if (item.is('[ui-portlet]')) {
            item.find('iframe:first').attr('src', value);
          }
          break;
        case 'value':
        case 'value:set':
          if (itemScope.setValue) {
            itemScope.setValue(value);
          }
          break;
        case 'value:add':
          if (itemScope.fetchData && itemScope.select) {
            itemScope.fetchData(value, function(records){
              itemScope.select(records);
            });
          }
          break;
        case 'value:del':
          if (itemScope.removeItems) {
            itemScope.removeItems(value);
          }
          break;
        }
      });

      if (hasValues && formScope.onChangeNotify) {
        formScope.onChangeNotify(formScope, formScope.record);
      }
    }

    forEach(data.attrs, function(itemAttrs, itemName) {
      var items = findItems(itemName);
      if (!items || items.length === 0) {
        // dashlet still not loaded ?
        if (itemName.indexOf('.') > -1) {
          var parentName = itemName.substring(0, itemName.indexOf('.'));
          var parentElem = findItems(parentName);
          if (parentElem.is('[ui-dashlet]')) {
            parentElem.scope().$$pendingAttrs = parentElem.scope().$$pendingAttrs || {};
            parentElem.scope().$$pendingAttrs[itemName.substring(itemName.indexOf('.')+1)] = itemAttrs;
          }
        }
        return;
      }
      items.each(function(i) {
        setAttrs($(this), itemAttrs, i);
      });
    });

    if (data.report) {
      return openReport(data);
    }

    function openReport(data) {
      var record = formScope.record || {};
      if (data.attached) {
        record.$attachments = (record.$attachments || 0) + 1;
        axelor.dialogs.confirm(_t('Report attached to current object. Would you like to download?'),
        function(confirmed) {
          scope.$applyAsync(function() {
            if (confirmed) {
              var url = "ws/rest/com.axelor.meta.db.MetaFile/" + data.attached.id + "/content/download";
              ui.download(url);
              return deferred.resolve();
            }
            deferred.reject();
          });
        }, {
          title: _t('Download'),
          yesNo: false
        });
        return deferred.promise;
      }

      var url = "ws/files/report/" + data.reportLink + "?name=" + data.reportFile;
      var tab = {
        title: data.reportFile,
        resource: url,
        viewType: 'html'
      };

      if (axelor.device.mobile && data.reportFormat !== "html") {
        ui.download(url, data.reportFile);
      } else if (['pdf', 'html'].indexOf(data.reportFormat) > -1) {
        doOpenView(tab);
      } else {
        ui.download(url);
      }

      scope.$timeout(deferred.resolve);
      return deferred.promise;
    }

    function openTab(scope, tab) {
      if (scope.openTab) {
        scope.openTab(tab);
      } else if (scope.$parent) {
        openTab(scope.$parent, tab);
      }
    }

    function doOpenView(tab) {
      tab.action = _.uniqueId('$act');
      if (!tab.viewType)
        tab.viewType = 'grid';
      if (tab.viewType == 'grid' || tab.viewType == 'form')
        tab.model = tab.model || tab.resource;
      if (!tab.views) {
        tab.views = [{ type: tab.viewType }];
        if (tab.viewType === 'html') {
          angular.extend(tab.views[0], {
            resource: tab.resource,
            title: tab.title
          });
        }
      }
      if (tab.viewType === "html" && (tab.params||{}).download) {
        var view = _.findWhere(tab.views, { type: "html" });
        if (view) {
          var url = view.name || view.resource;
          var fileName = tab.params.fileName || "true";
          ui.download(url, fileName);
          return scope.$applyAsync();
        }
      }
      if (tab.viewType === "html" && (tab.params||{}).target === "_blank") {
        var view = _.findWhere(tab.views, { type: "html" });
        if (view) {
          var url = view.name || view.resource;
          setTimeout(function () {
            window.open(url);
          });
          return scope.$applyAsync();
        }
      }
      if ((tab.params && tab.params.popup) || axelor.device.mobile) {
        tab.$popupParent = formScope;
      }
      openTab(scope, tab);
      scope.$applyAsync();
    }

    if (data.view) {
      doOpenView(data.view);
    }

    if (data.close || data.canClose) {
      this._closeView(scope);
    }

    deferred.resolve();

    return deferred.promise;
  }
};

ui.factory('ActionService', ['ViewService', function(ViewService) {

  function handler(scope, element, options) {
    var opts = _.extend({}, options, { element: element });
    return new ActionHandler(scope, ViewService, opts);
  }

  return {
    handler: handler
  };
}]);

var EVENTS = ['onClick', 'onChange', 'onSelect', 'onTabSelect', 'onNew', 'onLoad', 'onSave'];

ui.directive('uiActions', ['ViewService', function(ViewService) {

  function link(scope, element, attrs) {

    var props = _.isEmpty(scope.field) ? scope.schema : scope.field;
    if (!props) {
      return;
    }

    _.each(EVENTS, function(name){
      var action = props[name];
      if (!action) {
        return;
      }

      var handler = new ActionHandler(scope, ViewService, {
        name: name,
        element: element,
        action: action,
        canSave: props.canSave,
        prompt: props.prompt
      });
      scope.$events[name] = _.bind(handler[name], handler);
    });
  }

  return {
    link: function(scope, element, attrs) {
      scope.$evalAsync(function() {
        link(scope, element, attrs);
      });
    }
  };
}]);

ui.directive('uiActionClick', ['ViewService', function(ViewService) {
  return {
    link: function(scope, element, attrs) {
      var action = attrs.uiActionClick;
      scope.$evalAsync(function() {
        var handler = new ActionHandler(scope, ViewService, {
          element: element,
          action: action
        });
        element.on("click", function () {
          handler.handle();
          scope.$applyAsync();
        });
      });
    }
  };
}]);

})();
